LightPipe

Part 1: Software

Asterix and h8 @ OpenChaos November 2023

TOC

  1. History
  2. Overview
  3. lightpipe basics
  4. lightpipe advanced
  5. paintbag
  6. simulation
  7. run
  8. Future

MateLight (2018)

  • mostly the same -> much copy-pasted
  • refactoring
    • simulation, paintbag
  • new features
    • different LED-coordinates
    • more in paintbag
  • learning from mistakes
    • effect importing

Simplified overview







G



apa102 /\nsimulation.py

apa102 /
simulation.py



LightPipe (Hardware) /\ntk window (Software)

LightPipe (Hardware) /
tk window (Software)



apa102 /\nsimulation.py->LightPipe (Hardware) /\ntk window (Software)





lightpipe.py

lightpipe.py



lightpipe.py->apa102 /\nsimulation.py





some_effect.py

some_effect.py



some_effect.py->lightpipe.py





paintbag.py

paintbag.py



some_effect.py->paintbag.py





Overview







G



apa102

apa102



LightPipe (Hardware)

LightPipe (Hardware)



apa102->LightPipe (Hardware)





simulation.py\nas apa102

simulation.py
as apa102



tk window (Software)

tk window (Software)



simulation.py\nas apa102->tk window (Software)





lightpipe.py

lightpipe.py



lightpipe.py->apa102


good stuff



lightpipe.py->simulation.py\nas apa102


Simulation



run.py

run.py



run.py->lightpipe.py





paintbag.py

paintbag.py



run.py->paintbag.py





effects/*.py

effects/*.py



run.py->effects/*.py





lightpipe basics

lp = Lightpipe(
    pipes = 1,
    p_size = 16,
    wiring = "",
    brightness = 4,
    serial = "simulation",
)

Basic Usage

lp.set_pipe( p=0, color )
lp.set_pixel( p=2, x=3, color, overflow )
lp.clear( show=False, background )
lp.show()

color

  • 3-tuple of int8: (0, 0, 255)
  • String of hexadecimal notation: "d511ff"
  • more in paintbag

Simple effect

for p in range(lp.pipes): for x in range(lp.p_size): print(p,x) lp.clear(show=False) lp.set_pixel(p, x, (255,0,0)) lp.show() time.sleep(0.5) lp.set_pipe(0, (0,255,0)) lp.show()

lightpipe advanced

more features in the lightpipe core

Position Overflow handling

  • adjacent: draw overflowing pixels on all adjacent pipes. nyi
  • border: replace overflowing values with max or min.
  • discard: ignore overflowing values.
  • flow: like adjacent, but only either up- or downstream. nyi
  • modulo: overflow into the same pipe.
  • next: overflow into the (linear) next pipe.

Overflow code

if mode == "border": x = max(0, min(x, self.p_size-1)) if mode == "discard": # # pass if mode == "modulo": x = x % self.p_size if mode == "next": p += x // self.p_size x = x % self.p_size return p,x

Demo Time

Paste multiple colors

lp.fill(p, x, colors, overflow)
for i, color in enumerate(colors): self.set_pixel(p, x+i, color, overflow)

All methods with part pixel

lp.set_pixel_0( p, x, color, overflow )
lp.set_pixel_y( p, x, color, y, overflow )

lp.fill_0(p, x, colors, overflow)
lp.fill_y(p, x, colors, y, overflow)

wiring String

  • driver gets LightPipe wiring information
  • can calculate adjacency of pipes
  • The wiring string consists only of another pipe and back to previous pipe

ascii visulisation

input -> +-------+ -a-> +-------+ _
               | p = 0 |          | p = 1 |    b
               +-------+      / +-------+ <´
                                  a
     Fitting form: Y    `> +-------+ <- output
     string result: aba     | p = 2 |
                                        +-------+

Tree lut

  • lp.get_adjacent_pipes(p) -> set(p_ids)
  • based on wiring string, a k-tree look-up-table can be implemented
  • nyi, but math is done

paintbag

contains color helper functions

pb.

  • colors
  • random_color(saturated=True)
  • rgb_tuple(color)
  • rgb2hex(color)
  • hsv2rgb(h, s, v)
  • check_{saturation,hue}(color)
  • create_gradient(color1, color2, n)
  • saturate_gradient(gradient)
  • cgs(color1, color2, n, clockwise=False)

Beautiful effect

for p in range(lp.pipes): grad = pb.cgs( pb.random_color(), pb.random_color(), lp.p_size, ) lp.fill( p, 0, grad ) lp.show() time.sleep(5)

Simulation

  • no LightPipe? -> use simulation
  • written in python-tk
    • optional dependency of driver
  • replaces the apa102 driver






G



apa102

apa102



LightPipe (Hardware)

LightPipe (Hardware)



apa102->LightPipe (Hardware)





simulation.py\nas apa102

simulation.py
as apa102



tk window (Software)

tk window (Software)



simulation.py\nas apa102->tk window (Software)





lightpipe.py

lightpipe.py



lightpipe.py->apa102





lightpipe.py->simulation.py\nas apa102





Created with Raphaël 2.3.0effect+lightpipe.pyeffect+lightpipe.pysimulatorsimulatortk_deamon_threadtk_deamon_threadset_pixel() ; strip.show()stores values ; put data to queueshow()okeffect continuesdraws pixel in tk canvasdone

If you are interested?

Warning:
more complex code below

simulator thread

self.q = queue.Queue(maxsize=2) self.simulator_ready = False self.simulation = threading.Thread( target = self._simulator_daemon, daemon = True, ) self.simulation.start() while not self.simulator_ready: # time.sleep(0.1)

simulator deamon function

def _simulator_daemon(self): self.simulator = Simulator( pipes = self.pipes, p_size = self.p_size, q = self.q, ) self.simulator_ready = True self.simulator.run()

simulator show

def show(self): # # # # data = str(self.leds) self.q.put(data) self.simulator.main.event_generate("<<show>>")

tk event

self.main.bind("<<show>>", self.show)
def show(self, event): data = self.q.get() leds = ast.literal_eval(data) # for pos, color in enumerate(leds): self.set_pixel(pos, color) self.q.task_done()

reverse calc coordinates

def set_pixel(self, pos, color): pipe_pos = pos % (self.p_size * 3) p = pos // (self.p_size * 3) if pipe_pos >= self.p_size and pipe_pos < 2*self.p_size: x = self.p_size - pipe_pos % self.p_size -1 else: x = pipe_pos % self.p_size y = 3*p + pipe_pos // self.p_size

Running effects







G



lightpipe.py

lightpipe.py



run.py

run.py



run.py->lightpipe.py





paintbag.py

paintbag.py



run.py->paintbag.py





effects/*.py

effects/*.py



run.py->effects/*.py





Import all

import glob import importlib
ls = [_[8:-3] for _ in glob.glob("effects/*.py")] effect_dict = {} for ef in ls: effect_dict[ef] = importlib.import_module(f"effects.{ef}")

Execute all

from inspect import signature
f = effect_dict[ef].effect # if len(signature(f).parameters) == 1: try: f(lp) except: # elif len(signature(f).parameters) == 2: try: f(lp, pb) except: #

my_effect.py







G



lightpipe.py

lightpipe.py



run.py

run.py



run.py->lightpipe.py





paintbag.py

paintbag.py



run.py->paintbag.py





effects/*.py

effects/*.py



run.py->effects/*.py





my_effect.py

def effect(lp): color = (255,255,0) while True: now = time.localtime( time.time() ) d = lp.p_size * (now[3] * 60 + now[4]) // (24*60) lp.clear(show=False) lp.fill(0, 0, d*[color]) lp.show() time.sleep(100)

my_effect.py

if __name__ == "__main__": import lightpipe lp = lightpipe.Lightpipe() effect(lp)

Future Plans

until 37c3

  • config via file
  • tree_lut based on wiring
  • allow- or blocklist in run.py

Future Plan: 38c3

  • construct layer
    • between lightpipe and effect
    • to build specialised effects for specific constructs
  • state (maybe)
    • save & reload LED colors
  • color calibration

Future Plan: 39c3

  • 3D-printing like driver & effects
    • driver gets fitting information,
    • iterates over existing (x,y,z) LEDs and calls:
    • effect(t, (x,y,z) ) -> color

Future is not now